设计模式
设计模式是什么
设计模式是一个通过定义、使用、测试去解决特定问题的方法,是针对软件设计中在给定条件下会重复性发生的问题而提出的一种通用性的可重用解决方案,设计模式不是可以直接转化为代码的完整设计,它是用于描述在不同情况下解决问题的通用方案。
设计模式的作用
设计模式通过提供经过验证的行之有效的开发范式加快开发过程,预防重大的隐患问题,提高代码可读性。
设计模式的分类
这里主要讨论GoF所提出的23种设计模式,可将其分为三种类型:
- 创造型设计模式
- 结构型设计模式
- 行为型设计模式
创造型设计模式
注重完成对象的实例化,相比于直接实例化对象,根据实际情况选择合适的设计模式完成对象的实例化,可以为复杂的业务场景带来更高的灵活性。 创造型设计模式主要包括以下几种:
- 抽象工厂设计模式
- 生成器设计模式
- 工厂方法设计模式
- 原型设计模式
- 单例设计模式
结构型设计模式
结构型设计模式用于指导我们完成对代码的结构划分,如此,代 码结构会更加清晰,更易理解,也提高了软件的可维护性。 结构型设计模式主要包括以下几种:
- 适配器设计模式
- 桥接设计模式
- 组合设计模式
- 装饰设计模式
- 门面设计模式
- 享元设计模式
- 代理设计模式
行为型设计模式
行为型设计模式主要用于定义对象之间的通信与流程控制,主要的设计模式都非常注重优化对象之间的数据交互方式。 行为型设计模式主要包括以下几种:
- 职责链设计模式
- 命令设计模式
- 解释器设计模式
- 迭代器设计模式
- 中介者设计模式
- 备忘录设计模式
- 观察者设计模式
- 策略设计模式
- 状态设计模式
- 模板方法设计模式
- 访问者设计模式
如何学习设计模式
- 模式名称是什么?
- 模式类型是什么?是创造型,结构型,还是行为型?
- 模式的目的是什么?(作用是什么?解决了什么问题?)
- 模式的别名是什么?
- 什么情况下使用该模式?
- 该模式的基本示例
- 该模式的UML图是什么样的?是类图还是交互图?
- 都有那些对象在模式中参与活动?列出设计模式中使用的类和对象,并说明他们各自的角色
- 模式中的类和对象是怎么进行交互的?
- 通过应用设计模式能获取什么好处,有哪些坏处?如何权衡?
- 如何实现该模式
- 与该模式相近的设计模式是什么?这几种相近的模式有哪些异同点?
正确看待设计模式
恰当使用设计模式能够提高代码的复用性,但是由于复用性往往会引入封装与间接调用,这些会降低系统性能,增加代码复杂程度。因此,除非设计模式能够帮助我们完成代码的实现或者后续的维护工作,否则没有必要去引入设计模式。 学习设计模式的关键并不在于学习设计模式本身,而是在于识别应用场景与潜在的风险,并将设计模式用之有道,这般,设计模式才能算作得心应手的工具。 在没有必要的情况大可不必去使用设计模式,因为设计模式有可能会牺牲代码的简洁性,而且滥用设计模式多会引入新的问题却没有解决原来的问题。 保持代码的整洁,模块化和可读性,同时不要让各类之间过度耦合。
创造型设计模式
创造型设计模式主要关注的是类的实例化,也就是说体现的是对象的创建方法,利用这些模式,我们可以在适当的情况下以适当的形式创建对象,创造型设计模式通过控制对象的创建来解决设计中的问题。 创造型设计模式主要包含以下子类别:
- 对象创造型设计模式:
主要完成对象创建,并将对象中部分内容放到其他对象中创建。
- 类创造型设计模式:
主要完成类的实例化,并将类中的部分对象放到子类中创建,此类模式在实例化过程中高效地利用了继承机制 创造型设计模式主要包含以下5种具体的设计模式:
- 抽象工厂设计模式
提供一个用于创建相关对象或相互依赖对象的接口,无需指定对象的具体类
- 生成器设计模式
将复杂对象的构建与其表示相互分离,使得同样的构建过程可以创建不同的表示
- 工厂方法设计模式
允许在子类中实现本类的实例化类
- 原型设计模式
使用一个原型实例来指定创建对象的种类,然后通过拷贝这些原型实现新对象的创建
- 单例模式
确保某个类在系统中仅有一个实例,并提供一个访问它的全局访问点
对象创造型设计模式 | 类创造型设计模式 |
---|---|
抽象工厂设计模式 | 工厂方法设计模式 |
生成器设计模式 | |
原型设计模式 | |
单例设计模式 |
工厂方法设计模式
工厂方法的作用是创建对象,用来从一组实现特定逻辑的类中实例化某个对象。
模式中包括的类
- 产品类(Product)中定义了工厂方法创建的对象接口。
- 具体产品类( Concrete Product)实现产品类接口。
- 工厂类( Creator,因为由它来创建产品类,所以叫作工厂类)声明工厂方法,返回一个产品类对象。可用于调用创建产品类对象的生成方法。
- 具体工厂类( Concrete Creator)重写用于创建具体产品类对象的方法。
UML图
功能及应用场景
- 当需要创建一个类,而在编程时不能确定这个类的类型时(需要运行时确定)。
- 当一个类希望由其子类来指定所创建对象的具体类型时。
- 当我们想要定位被创建类,并获取相关信息时。
抽象工厂设计模式
抽象工厂模式相比于工厂方法模式的抽象层次更高。这意味着抽象工厂返回的是一组类的工厂。与工厂方法模式类似(返回多个子类中的一个),此方法会返回一个工厂,而这个工厂会返回多个子类中的一个。简单来说,抽象工厂是一个工厂对象,该对象又会返回若干工厂中的一个。 工厂模式是创造型模式的典型示例。抽象工厂设计模式是工厂方法模式的扩展,从而使我们无须担心所创建对象的实际类就能够创建对象。抽象工厂模式扩展了工厂方法模式,允许创建更多类型的对象。
模式中包括的类
- 抽象工厂(AbstractFactory)声明一个用于完成抽象产品对象创建操作的接口。
- 具体工厂(ConcreteFactory)实现创建具体产品对象的操作。
- 抽象产品(AbstractProduct)声明一个用于一类产品对象的接口。
- 具体产品(ConcreteProduct)定义由相应的具体工厂来创建的产品对象。
- 客户端(Client)使用由抽象工厂和抽象产品类声明的唯一接口。
UML图
功能及应用场景
抽象工厂模式的主要优点之一是它屏蔽了这些具体类的创建方法。实际应用的类名称不需要再让客户端(将客户端与具体类解耦)知道。由于具体类是屏蔽的,因此我们可以在不同的工厂(实现方法)之间进行切换。
生成器设计模式
生成器模式,能够从简单的对象一步一步生成复杂的对象。生成器模式是一种用来逐步构建复杂对象并在最后一步返回对象的创造型模式。 构造一个对象的过程是通过泛型实现的,以便它能够用于对同一对象创建不同的表示形式。
模式中包括的类
- 生成器类( Builder)提供一个接口用于创建产品的各个组成部件。具体生成器(Concrete Builder)提供此接的实现。
- 具体生成器( ConcreteBuilder)会跟踪其所创建对象的表现形式,并在创建对象的同时提供一个接口获取产品(Product)。
- 导演类( Director)通过生成器提供的接口构造对象。产品类用于表示被构造的复杂对象。这包括对我们构建的所有类进行定义。
UML图
功能及应用场景
生成器模式隐藏了产品构建过程中的内部细节。各个生成器之间都是相互独立的。这提高了代码的模块化,并使其他的生成器更方便地创建对象。因为每个生成器都能够逐步创建对象,这让我们能够很好地对最终产品进行掌控。
单例设计模式
在应用程序的整个生命周期中,对象只有一个实例的时候,就会使用单例设计模式。单例类总是在第一次被访问时完成实例化,直至应用程序退出之前,都只会使用同一个实例。单一实例创建策略:我们通过限制构造函数(通过设置其为私有)从而限制单例类的实例化。之后在定义类时包含一个该类的静态私有对象,以便创建单例类的实例。 在单例模式中,最棘手的部分是对单一实例的实现和管理。 在单例模式的定义过程中,有两点需要注意的地方:
- 该类仅允许存在一个实例。
- 需要为该单一实例提供一个全局访问点。
单例模式中的主动实例化和被动实例化(饿汉、懒汉) 线程安全的单例:双重同步锁、静态变 量、枚举
模式中包括的类
- 单例类
UML图
功能及应用场景
在应用程序的整个生命周期中,对象只有一个实例的时候,就会使用单例设计模式。
原型设计模式
相比于以往创建一个复杂对象总是费时费力,原型模式只需要复制现有的相似对象,并根据需要做适当修改。原型意味着使用克隆方法。克隆方法是一种复制对象的操作。克隆出的对象副本被初始化为调用克隆方法时原始对象的当前状态。这意味着对象的克隆避免了创建新对象。如果创建一个新对象的开销很大,而且有可能引起资源紧张时,我们就克隆对象。
- 浅层复制:当原始对象变化时,新对象也跟着改变。这主要是因为浅层复制并没有实际复制新的对象,而只是对原有对象的一个引用。
- 深层复制:当原始对象变化时,新对象不受影响,因为原始对象所包含的所有参数、对象和引用在复制新对象的过程中都建立了新的拷贝。
使用克隆方法来复制对象时,具体是使用浅层复制还是深层复制是由业务需求来决定的。在使用原型模式时,使用克隆方法来复制对象仅仅是一个设计上的决策。克隆方法对于原型模式来说并不是强制性的最佳选择。
模式中包括的类
- 客户端(Client):通过调用原型类的克隆操作创建一个新对象。
- 原型类( Prototype):声明一个接口用于克隆自己。
- 具体原型类( Concrete Prototype):实现克隆自己的操作。
UML图
功能及应用场景
- 当一个系统应该独立于其产品的创建、组合和表示。
- 当需要实例化的类是在运行时定义的,例如动态加载,或避免建立一个平行于产品类继承层次的工厂类继承层次时。
- 当一个类的实例仅可以拥有若干不同的状态组合中的一个时。使用原型模式建立相应数量的原型和克隆方法,会比每次都手动实例化类并配置相应状态更加方便。
主要难点:
- 每个原型类的子类都必须实现克隆操作。这实现起来可能有难度。例如,当类已经存在的时候添加克隆方法可能比较困难。
- 对象内部包含其他不支持克隆的对象或具有循环引用的对象时,实现克隆方法会比较困难。
优点:
- 原型模式意味着使用克隆方法。克隆方法是一种复制对象的操作。相比于耗时的复制对象创建过程,原型模式仅复制类似的现有对象,再根据需要对复制出的副本进行修改。
- 客户端可以在运行时添加或移除原型对象。
- 通过各种参数来定义新对象:高度动态的系统允许我们通过使用对象组合来定义新的特征,例如为对象变量指定相应的参数值,而不是重新定义一个类。我们通过实例化现有类可以有效地定义新类型的对象,并为客户端对象注册原型实例。客户端可以通过向原型类委派某个责任而使其具有新的特征。这种设计允许用户无须大量编程就能轻松定义新的类。事实上,克隆一个原型本质上是类似于类的实例化的。但原型模式能够大大降低系统所需的类的数量。
副作用:
- 使用原型模式,我们可以根据需要通过对象克隆来实现运行时对象的添加和删除。我们可以根据程序运行情况在运行时修改类的内部数据表示形式。
- 在Java中实现原型模式的一大困 难是如果这些类已经存在,我们未必能够通过添加所需要的克隆方法或深层克隆方法对类进行修改。此外,那些与其他类具有循环引用关系的类并不能真正实现克隆。
- 需要在这些类中具有足够的数据访问权限或方法,以便在克隆完成后对相应的数据进行修改。这可能需要在这些原型类中添加相应的数据访问方法,以便我们对类完成克隆之后可以修改数据。
结构型设计模式
结构型模式主要描述如何将对象和类组合在一起以组成更复杂的结构。在软件工程中结构型模式是用于帮助设计人员通过简单的方式来识别和实现对象之间关系的设计模式。结构型模式会以组的形式组织程序。这种划分形式使代码更加清晰,维护更加简便。结构型模式用于代码和对象的结构组织。 结构型模式会以组的形式组织程序。这种划分形式使代码更加清晰,维护更加简便。 结构型模式又分为以下子类别:
- 对象结构型模式:用于对象之间相互关联与组织,以便形成更大、更复杂的结构。
- 类结构型模式:用于实现基于继承的代码抽象,并且会介绍如何通过该模式提供更有用的程序接口。
具体包括:
对象结构型模式 | 类结构型模式 |
---|---|
桥接模式 | 类适配器模式 |
组合模式 | |
装饰模式 | |
门面模式 | |
享元模式 | |
对象适配器模式 | |
代理模式 |
- 组合模式:它能够为客户端处理各种复杂和灵活的树状结构。这些树结构可以由各种不同类型的容器和叶节点组成,其深度或组合形式能够在运行时调整或确定。
- 装饰模式:允许我们通过附加新的功能或修改现有功能,在运行时动态地修改对象。
- 门面模式:允许我们为客户端创建一个统一的接口以访问不同子系统的不同接口,从而简化客户端。
- 享元模式:客户端调用类时会在运行时创建大量对象,该模式会重新设计类以优化内存开销。
- 代理模式:为其他对象提供一种代理以控制对这个对象的访问。这种模式的目的是一个对象不适合或者不能直接引用另一个对象,简化客户端并实现对象访问,同时避免任何副作用。
- 适配器模式:允许我们为一个已有的类提供一个新的接口,并在客户端请求不同接口时实现类的重用。
- 桥接模式:允许我们将类与其接口相互解耦。允许类及其接口随着时间相互独立变化,增加类重用的次数,提高后续可扩展性。它也允许运行时对接口的不同实现方式动态切换,使代码更加灵活。
适配器设计模式
软件适配器的工作原理也和插座适 配器完全一样。我们也经常需要在程序中使用到不同的类或模块。假设有一段代码写得很烂,如果我们直接将这些代码集成到程序中,会将现有的代码搞乱。但是我们又不得不调用这段代码,因为我们需要实现相关的功能,而从头写起会耽误很多宝贵的时间。这时的最佳实践就是编写适配器,并将所需要的代码包装进去。这样我们就能够使用自定义的接口,从而降低对外部代码的依赖。 适配器模式会将现有接口转换为新的接口,已实现对应用程序中不相关的类的兼容性和可重用性的目标。适配器模式也被称为包装模式。适配器模式能够帮助那些因为接口不兼容而无法一起工作的类,以便它们能够一同工作。 适配器模式也负责将数据转换成适当的形式。当客户端在接口中指定了其对数据格式的要求时,我们通常可以创建新的类以实现现有类的接口和子类。这种实现方式也会通过创建类适配器,实现对客户端调用命和现有类中被调用方法之间接口的转换。
模式中包括的类
- 客户端(Client)调用目标类的类或程序。
- 目标类(Target)客户端想要使用的接口。
- 适配对象类(Adapetee)需要进行适配的类或对象。
- 适配器类( Adapter)按照目标类接口的要求对适配对象接口实现接口形式的适配转换。
- request方法:客户端想要执行的操作。
- specificRequest方法:适配对象中能够完成 request方法功能的实现。
UML图
功能及应用场景
在具体实践上,有两种实际应用适配器模式的方法:
- 使用继承[类适配器]
- 使用关联[对象适配器]
应用场景:
- 我们想要使用现有的类,但它的接口不符合我们的需要。
- 我们想要创建一个可重用的类,能够与一些无关的类或不可预见的类进行协作,同时这个类无须具有兼容的接口。
- (仅适用于对象适配器)我们需要使用多个已经存在的子类,而我们为每一个子类都做接口适配显然是不切实际的。使用对象适配器可以直接适配其父类的接口。
桥接设计模式
桥接模式是结构型模式中的另一个典型模式。桥接模式用于将类的接口与接口的实现相互解耦。这样做提高了系统的灵活性使得接口和实现两者均可独立变化。 举一个例子,让我们想一下家用电器及其开关。例如,风扇的开关。开关是电器的控制接口,而一旦闭合开关,实际让风扇运转的是风扇电机。 所以,在这个示例中,开关和风扇之间是彼此独立的。如果我们将开关接到电灯泡的供电线路上,那么我们还可以选用其他开关来控制风扇。
模式中包括的类
- 抽象化对象(Abstraction)桥接设计模式的核心,并定义了关键症结所在。包含对实现化对象的引用。
- 扩充抽象化对象(RefinedAbstraction)扩展抽象化对象,并将抽象化对象细化到新的层次。对实现化对象隐藏细节元素。
- 实现化对象(Implementor)该接口比抽象化对象的层次更高。只对基本操作进行定义。
- 具体实现化对象(Concretelmplementor)通过提供具体实现来执行实现化对象的具体功能。
UML图
功能及应用场景
桥接模式主要适用于系统的多个维度上都经常发生变化的情况。桥接模式能够将不同的抽象维度进行衔接。通过桥接模式,抽象化对象 和实现化对象不会在编译时进行绑定,而能够在各自的类被调用时独立扩展。 当你经常需要在运行时在多个实现之间进行切换时,桥接模式也非常有用。
组合设计模式
在大部分系统开发过程中,程序员都会遇到某个组件既可以是独立的个体对象,也能够作为对象集合的情况。组合模式就用于此类情况的设计。简单来说,组合模式是一组对象的集合,而这组对象中的每一个对象本身也是一个组合模式构成的对象,或者只是一个原始对象。 组合模式中存在着一个树形结构,并且在该结构中的分支节点和叶节点上都能够执行相同的操作。树形结构中每一个分支节点都包含子节点的类(能继承出叶节点和分支节点),这样的分支节点本身就是一个组合模式构成的节点。树形结构中的叶子节点仅是一个原始对象,其没有子节点(不能继承出叶节点和分支节点)。组合模式的子类(下一级节点)可以是叶子节点或其他组合模式。
模式中包括的类
- 组件对象:(Component,结构)
- 组件对象在整个继承结构的最顶端。它是对组合模式的抽象。
- 它声明了组合模式中的对象接口。
- 可以选择性地定义一个接口,以便对递归结构中组件的父类进行访问,并在需要的时候实现该接口。
- 叶子节点:(Leaf,原始对象)
- 树形结构的末端且不会再有子节点。
- 定义了组合结构中单个对象的行为。
- 分支节点类:(Composite,组)
- 包含了子组件并为它们定义行为。
- 实现子节点的相关操作。
UML图
功能及应用场景
- 当对象的集合需要采用与单个对象相同的处理方式时。
- 操纵单个对象的方式与操纵一组对象的方式类似时。
- 注意存在能够组合的递归结构或树形结构。
- 客户端能够通过组件对象访问整个继承结构,而它们却不会知道自己所处理的是叶子节点还是分支节点。
组合模式的目的是能够使独立对象(单个分支节点或叶子节点)和对象集合(子树)都能够以同样的方式组织起来。组合模式中所有的对象都来自于其本身(成为一种嵌套结构)。组合模式允许我们使用递归的方式将类似的对象组合成一种树形结构,来实现复杂结构对象的构建。